<?php

namespace pmvc\mvc;

use pmvc\util\StringUtil;
use pmvc\log\Log;
use Exception;

class Request {

	const PARAMETERS 		= "_REQUESTPARAMS";
	const SERVER_VARIABLES 	= "_SERVERVARS";
	const PATH_VARIABLES 	= "_PATHVARS";
	const URI 				= "_REQUESTURI";
	const HEADERS 			= "_REQUESTHEADERS";
	const ATTRIBUTES		= "_REQUESTATTRIBUTES";
	const COOKIES			= "_COOKIES";
	const FILES				= "_FILES";

	private $dispatcher;
	private $url;
	private $requestedUrl;
	private $contextUrl;
	private $baseUrl;
	private $secure 		= false;

	private $attributes 	= null;
	private $headers 		= null;
	private $pathVars 		= null;
	private $requestParams	= null;
	private $serverVars		= null;
	private $cookies		= null;
	private $files			= null;

	public static function factory(Request $request, $overrides=Array()) {
		$requestParams = isset($overrides[self::PARAMETERS]) 
			? $overrides[self::PARAMETERS]
			: $request->requestParams;
			
		$serverVars = isset($overrides[self::SERVER_VARIABLES]) 
			? $overrides[self::SERVER_VARIABLES]
			: $request->serverVars;
			
		$pathVars = isset($overrides[self::PATH_VARIABLES]) 
			? $overrides[self::PATH_VARIABLES]
			: $request->pathVars;
			
		$headers = isset($overrides[self::HEADERS]) 
			? $overrides[self::HEADERS]
			: $request->headers;
			
		$attributes = isset($overrides[self::ATTRIBUTES]) 
			? $overrides[self::ATTRIBUTES]
			: $request->attributes;
			
		$url = isset($overrides[self::URI]) 
			? $overrides[self::URI]
			: $request->url;
			
		$cookies = isset($overrides[self::COOKIES]) 
			? $overrides[self::COOKIES]
			: $request->cookies;
			
		$files = isset($overrides[self::FILES]) 
			? $overrides[self::FILES]
			: $request->files;
			
		return new Request($request->dispatcher,
			$url, $requestParams, $pathVars, $serverVars,
			$headers, $attributes, $cookies, $files);
	}

	/**
	 * Creates a Request object for the given {@link Dispatcher}
	 * and url.
	 * @param Dispatcher $dispatcher
	 * @param string $url
	 */
	public function __construct(Dispatcher $dispatcher, $url=null,
		$requestParams=null, $pathVars=null, $serverVars=null, $headers=null, $attributes=null, $cookies=null, $files=null) {

		// set default avlues
		$this->dispatcher 		= $dispatcher;
		$this->requestParams	= isset($requestParams) ? $requestParams : $_REQUEST;
		$this->pathVars			= isset($pathVars) ? $pathVars : Array();
		$this->serverVars		= isset($serverVars) ? $serverVars : $_SERVER;
		$this->attributes		= isset($attributes) ? $attributes : Array();
		$this->url				= isset($url) ? $url : $this->serverVars['REQUEST_URI'];
		$this->cookies			= isset($cookies) ? $cookies : $_COOKIE;
		$this->files			= isset($files) ? $files : $_FILES;

		// special case for headers
		if (!isset($headers)) {
			
			// use the apache module
			if (function_exists("apache_request_headers")) {
				$this->headers = apache_request_headers();
				
			// parse manually
			} else {
				$this->headers = Array();
				foreach ($this->serverVars as $k => $v) {
					if (substr($k, 0, 5) == "HTTP_") {
						$k = str_replace('_', ' ', substr($k, 5));
						$k = str_replace(' ', '-', ucwords(strtolower($k)));
						$this->headers[$k] = $v;
					}
				}
			}
		} else {
			$this->headers = $headers;
		}

		// we need a request url
		if (!isset($this->url)) {
			throw new Exception("Invalid request url");
		}

		// get query string
		if (($idx=strpos($this->url, '?'))!==false) {
			$this->url = substr($this->url, 0, $idx);
		}
		Log::trace(__CLASS__, "url parsed as {0}", $this->contextUrl);

		// parse context url
		$parts = explode("/", $this->serverVars["SCRIPT_NAME"]);
		$this->contextUrl = (count($parts)>0)
			? str_replace("/".$parts[count($parts)-1], "", $this->serverVars["SCRIPT_NAME"])
			: "";
		Log::trace(__CLASS__, "contextUrl parsed as {0}", $this->contextUrl);

		// parse requested url
		if ($this->contextUrl!="") {
			$pos = strpos($this->url, $this->contextUrl);
			$this->requestedUrl = ($pos!==false && $pos==0)
				? substr($this->url, strlen($this->contextUrl))
				: $this->url;
		} else {
			$this->requestedUrl = $this->url;
		}
		Log::trace(__CLASS__, "requestedUrl parsed as {0}", $this->requestedUrl);

		// check to see if it's secure
		$this->secure = array_key_exists("HTTPS", $this->serverVars);
		Log::trace(__CLASS__, "Is secure: {0}", $this->secure);

		// parse baseUrl
		$http = array_key_exists("HTTPS", $this->serverVars) ? "https" : "http";
		if (array_key_exists("HTTP_HOST", $this->serverVars)) {
			$this->baseUrl .= $http."://".$this->serverVars["HTTP_HOST"];
		} else if (array_key_exists("SERVER_NAME", $this->serverVars)) {
			$this->baseUrl .= $http."://".$this->serverVars["SERVER_NAME"];
		} else if (array_key_exists("SERVER_ADDR", $this->serverVars)) {
			$this->baseUrl .= $http."://".$this->serverVars["SERVER_ADDR"];
		}
		if (strlen($this->baseUrl)>0
			&& array_key_exists("SERVER_PORT", $this->serverVars)
			&& $this->serverVars["SERVER_PORT"]!=80
			&& $this->serverVars["SERVER_PORT"]!=443) {
				$this->baseUrl .= ":".$this->serverVars["SERVER_PORT"];
		}
		$this->baseUrl .= $this->contextUrl;
		Log::trace(__CLASS__, "baseUrl parsed as {0}", $this->baseUrl);
	}

	/**
	 * Parses path variables from the given {@link MappedObject}'s
	 * mapping and the {@link Request}'s requested url and populates
	 * the {@link Request}'s path variables.
	 * @param MappedObject $mapping the mapping
	 */
	public function addPathVariables(MappedObject $mapping) {
    	$pathVars = StringUtil::extractNamedMatches($this->requestedUrl, $mapping->getMapping());
		if ($pathVars!=null && is_array($pathVars)) {
			foreach ($pathVars as $pVar => $pVal) {
				$this->pathVars[$pVar] = $pVal;
			}
		}
	}

	/**
	 * Returns the {@link Dispatcher} that created this Request.
	 */
	public function getDispatcher() {
		return $this->dispatcher;
	}

	/**
	 * Checks to see if the request is a secure request.
	 * @return a boolean
	 */
	public function isSecure() {
		return $this->secure;
	}

	/**
	 * Returns the URL that was requested.
	 * @return string the URL
	 */
	public function getRequestedUrl() {
		return $this->requestedUrl;
	}

	/**
	 * Returns the context URL
	 * @return string the context URL
	 */
	public function getContextUrl() {
		return $this->contextUrl;
	}

	/**
	 * Returns the context URL
	 * @return string the context URL
	 */
	public function getBaseUrl() {
		return $this->baseUrl;
	}

	/**
	 * Returns the full url as requested
	 * by the web browser.
	 */
	public function getFullUrl($includeQueryString=true) {
		$qs = $this->serverVars["QUERY_STRING"];
		return $this->baseUrl
			.$this->requestedUrl
			.((!empty($qs) && $includeQueryString) ? "?".$qs : "");
	}

	/**
	 * Returns the query string.
	 */
	public function getQueryString() {
		return $this->serverVars["QUERY_STRING"];
	}

	/**
	 * Returns all of the client's request headers.
	 * @return Array
	 */
	public function getHeaders() {
		return $this->headers;
	}

	/**
	 * Returns the given header.
	 * @param string $name
	 */
	public function getHeader($name) {
		$headers = $this->getHeaders();
		$name = strtolower($name);
		foreach ($headers as $header => $val) {
			if (strtolower($header)==$name) {
				return $val;
			}
		}
		return null;
	}

	/**
	 * Indicates whether or not the given header was
	 * given to us by the client.
	 * @param string $name the name
	 */
	public function containsHeader($name) {
		$headers = $this->getHeaders();
		return array_key_exists(strtolower($name), $headers);
	}

	/**
	 * Returns all of the request parameters.
	 */
	public function getParameters() {
		return isset($this->requestParams) ? $this->requestParams : Array();
	}
	
	/**
	 * Returns the value of a request parameter, or the default
	 * value provided.
	 * @param string $name the name of the request param
	 * @param mixed $default the default value
	 */
	public function getParameter($name, $default=null) {
		return $this->getValue($this->requestParams, $name, $default);
	}

	/**
	 * Checks to see if a request parameter exists.
	 * @param string $name
	 */
	public function hasParameter($name) {
		return $this->getValue($this->requestParams, $name, null) != null;
	}

	/**
	 * Returns all of the uploaded files.
	 * @return array of UploadedFile
	 */
	public function getUploadedFiles() {
		if (!isset($this->files)) {
			return Array();
		}
		$ret = Array();
		foreach ($this->files as $file) {
			$ret []= $this->toUploadedFile($file);
		}
		return $ret;
	}
	
	/**
	 * Returns an uploaded file.
	 * @param string $name the name of the file
	 * @return UploadedFile or array of UploadedFile
	 */
	public function getUploadedFile($name) {
		$files = $this->getValue($this->files, $name, null);
		if (!isset($files) || empty($files)) {
			return null;
		} else if (isset($files["tmp_name"]) || isset($files["error"])
			|| isset($files["name"]) || isset($files["type"])
			|| isset($files["size"])) {
			return $this->toUploadedFile($files);
		}
		$ret = Array();
		foreach ($files as $file) {
			$ret []= $this->toUploadedFile($file);
		}
		return $ret;
	}

	/**
	 * Checks to see if an uploaded file exists.
	 * @param string $name
	 */
	public function hasUploadedFile($name) {
		return $this->getValue($this->files, $name, null) != null;
	}

	/**
	 * Creates an uploaded file.
	 * @param array $file
	 */
	private function toUploadedFile($file) {
		if (isset($file["error"]) && $file["error"]==UPLOAD_ERR_NO_FILE) {
			return null;
		} else if (isset($file["tmp_name"]) || isset($file["error"])
			|| isset($file["name"]) || isset($file["type"])
			|| isset($file["size"])) {
			return new UploadedFile($file);
		}
		return null;
	}

	/**
	 * Returns an array of all pathVariables set.
	 */
	public function getPathVariables() {
		return $this->pathVars;
	}

	/**
	 * Returns an pathVariable by it's name or the
	 * (optional) default value if it doesn't exist.
	 * @param string $name the pathVariable's name
	 * @param mixed $default the default value
	 */
	public function getPathVariable($name, $default=null) {
		return ($this->hasPathVariable($name))
			? $this->pathVars[$name] : $default;
	}

	/**
	 * Checks to see if the Request has an pathVariable.
	 * @param string $name
	 */
	public function hasPathVariable($name) {
		return array_key_exists($name, $this->pathVars);
	}

	/**
	 * Returns an array of all attributes set.
	 */
	public function getAttributes() {
		return $this->attributes;
	}

	/**
	 * Returns an attribute by it's name or the
	 * (optional) default value if it doesn't exist.
	 * @param string $name the attribute's name
	 * @param mixed $default the default value
	 */
	public function getAttribute($name, $default=null) {
		return ($this->hasAttribute($name))
			? $this->attributes[$name] : $default;
	}

	/**
	 * Checks to see if the Request has an attribute.
	 * @param string $name
	 */
	public function hasAttribute($name) {
		return array_key_exists($name, $this->attributes);
	}

	/**
	 * Sets an attribute's value by it's name.
	 * @param string $name the name of the attribute
	 * @param mixed $value the value
	 */
	public function setAttribute($name, $value) {
		$this->attributes[$name] = $value;
	}

	/**
	 * Removes an attribute by it's name.
	 * @param string $name the name
	 */
	public function removeAttribute($name) {
		unset($this->attributes[$name]);
	}

	/**
	 * Returns the current user's session id.
	 */
	public function getSessionId() {
		return session_id();
	}

	/**
	 * Returns an array of all session attributes set.
	 */
	public function getSessionVariables() {
		return isset($_SESSION) ? $_SESSION : Array();
	}

	/**
	 * Returns a session attribute by it's name or the
	 * (optional) default value if it doesn't exist.
	 * @param string $name the attribute's name
	 * @param mixed $default the default value
	 */
	public function getSessionVariable($name, $default=null) {
		return ($this->hasSessionVariable($name))
			? $_SESSION[$name] : $default;
	}

	/**
	 * Checks to see if the session has an attribute.
	 * @param string $name
	 */
	public function hasSessionVariable($name) {
		return array_key_exists($name, $_SESSION);
	}

	/**
	 * Sets a session attribute's value by it's name.
	 * @param string $name the name of the attribute
	 * @param mixed $value the value
	 */
	public function setSessionVariable($name, $value) {
		$_SESSION[$name] = $value;
	}

	/**
	 * Removes a session attribute by it's name.
	 * @param string $name the name
	 */
	public function removeSessionVariable($name) {
		unset($_SESSION[$name]);
	}

	/**
	 * Returns all server variables.
	 */
	public function getServerVariables() {
		return isset($this->serverVars) ? $this->serverVars : Array();
	}
	
	/**
	 * Returns the value of a request serverVariable, or the default
	 * value provided.
	 * @param string $name the name of the request param
	 * @param mixed $default the default value
	 */
	public function getServerVariable($name, $default=null) {
		return $this->getValue($this->serverVars, $name, $default);
	}

	/**
	 * Checks to see if a server variable exists.
	 * @param string $name
	 */
	public function hasServerVariable($name) {
		return $this->getValue($this->serverVars, $name, null) != null;
	}

	/**
	 * Returns an array of all cookies.
	 */
	public function getCookies() {
		return isset($this->cookies) ? $this->cookies : Array();
	}

	/**
	 * Returns a cookie by it's name or the
	 * null if it doesn't exist.
	 * @param string $name the cookie's name
	 */
	public function getCookie($name, $default=null) {
		return $this->getValue($this->cookies, $name, $default);
	}

	/**
	 * Checks to see if the request has a cookie.
	 * @param string $name
	 */
	public function hasCookie($name) {
		return $this->getValue($this->cookies, $name, null) != null;
	}

	/**
	 * Returns the request method sent by the user agent.
	 * @return string the request method
	 */
	public function getMethod() {
		return $this->serverVars["REQUEST_METHOD"];
	}

	/**
	 * Returns a value from an array.
	 * @param array $array
	 * @param string $name
	 * @param unknown_type $defaultValue
	 */
	private function getValue(array $array, $name, $defaultValue) {

		// do we want an array?
		$wantsArray = StringUtil::endsWith($name, "[]");

		// automatically find array names
		$name = ($wantsArray)
			? substr($name, 0, strlen($name)-2) : $name;
		$arrayName = ($wantsArray)
			? $name : $name."[]";

		// the value to return
		$value = null;

		// bail early if possible
		if (!isset($array) || empty($array)) {
			$value = $defaultValue;
		// find non array
		} else if (array_key_exists($name, $array) && isset($array[$name])) {
			$value = $array[$name];
		// find array
		} else if (array_key_exists($arrayName, $array) && isset($array[$arrayName])) {
			$value = $array[$arrayName];
		// default
		} else {
			$value = $defaultValue;
		}

		// return it
		return ($wantsArray && !is_array($value) && !empty($value)) 
			? Array($value) : $value;
	}
	
}

?>